﻿namespace KeyStone.EventHub
{
   using Collections.Concurrent;
   using System;
   using System.Collections.Generic;
   using System.Diagnostics;
   using System.Linq;
   using System.Threading;
   using System.Windows;
   using System.Windows.Threading;
   using Utility;

   /// <summary>
   /// Class EventHub.
   /// </summary>
   /// <remarks>
   /// Event Publication and Subscription
   /// </remarks>
   public sealed class EventHub
   {

      /// <summary>
      /// Gets or sets the trace enabled flag.
      /// </summary>
      /// <value>The trace enabled.</value>
      /// <remarks>
      /// When enabled, data is output to the console window</remarks>
      public static bool TraceEnabled { get; set; }

      /// <summary>
      /// The event dictionary
      /// </summary>
      private static ObservableConcurrentDictionary<string, object> eventDictionary;

      /// <summary>
      /// Initializes a new instance of the <see cref="EventHub"/> class.
      /// </summary>
      static EventHub()
      {
         eventDictionary = new ObservableConcurrentDictionary<string, object>();
      }

      /// <summary>
      /// Unsubscribe the specified event name.
      /// </summary>
      /// <exception cref="ArgumentNullException">if the event name is null or empty</exception>
      public static void Unsubscribe<TPayload>() where TPayload : Payload
      {
         string eventName = typeof(TPayload).Name.ToLower();
         lock (eventDictionary)
         {
            if (eventDictionary.ContainsKey(eventName))
            {
               Log("Unsubscribe {0}", eventName);
               eventDictionary.Remove(eventName);
            }
         }
      }

      /// <summary>
      /// Unsubscribe all events
      /// </summary>
      public static void UnsubscribeAll()
      {
         Log("Unsubscribe all publishers and subscribers");

         lock (eventDictionary)
         {
            eventDictionary.Clear();
            eventDictionary = new ObservableConcurrentDictionary<string, object>();
         }
      }

      /// <summary>
      /// Publishes the specified name.
      /// </summary>
      /// <typeparam name="TPayloadData">The type of the payload data.</typeparam>
      /// <param name="payload">The payload data to publish</param>
      /// <param name="sync">The synchronize.</param>
      /// <param name="runOnUiThread">if set to (default)<c>true</c> the publication executes on the UI Thread.</param>
      /// <param name="isDisposable">if set to <c>true</c> is the payload data can be disposed as soon as its published.
      /// Setting this to false will cause the last published data to persist, this means that any subsequent publication will receive multiple
      /// data publication events.</param>
      /// <exception cref="ArgumentNullException">if the event name or payload are null or empty</exception>
      /// <example>
      ///   <code>
      ///   [ConnectionState.cs]
      ///   public class ConnectionState : EventArgs
      ///   {
      ///      public string Address { get; set; }
      ///      public int Port { get; set; }
      ///      public ConnectionState(string address, int port)
      ///      {
      ///         Address = address;
      ///         Port = port;
      ///      }
      ///   }
      /// 
      ///   [Publisher.cs]
      ///   public class Publisher
      ///   {
      ///      public void Run()
      ///      {
      ///         // Publish the Connection event data
      ///         EventHub.Publish&lt;ConnectionState&gt;(new ConnectionState("127.0.0.1","8080"));
      ///      }
      ///   }
      /// 
      ///   [Subscriber.cs]
      ///   public class Subscriber
      ///   {
      ///      public void Run()
      ///      {
      ///         // Subscribes to the Connection Event
      ///         EventHub.Subscribe&lt;ConnectionState&gt;(SubscribedMethod);
      ///      }
      ///      public void SubscribedMethod(ConnectionState state, IEventSync sync)
      ///      {
      ///         Console.WriteLine(string.Format("http://{0}:{1}/", state.Address, state.Port));
      ///      }
      ///   }
      /// </code>
      /// </example>
      public static void Publish<TPayloadData>(TPayloadData payload, bool runOnUiThread = true, bool isDisposable = true)
          where TPayloadData : Payload
      {
         lock (eventDictionary)
         {
            string eventName = typeof(TPayloadData).Name.ToLower();

            if (eventDictionary.ContainsKey(eventName))
            {
               GetEventAndPublish(payload, runOnUiThread, eventName);
            }
            else
            {
               CreateEventAndAddPayload(payload, eventName, isDisposable);
            }
         }
      }

      /// <summary>
      /// Subscribes the specified name.
      /// </summary>
      /// <typeparam name="TPayloadData">The type of the t payload data.</typeparam>
      /// <param name="callback">The callback method that will receive the published data</param>
      /// <param name="runOnUiThread">if set to (default)<c>true</c> the publication executes on the UI Thread.</param>
      /// <exception cref="ArgumentNullException">if the event name or callback is null or empty</exception>
      /// <example>
      /// <code>
      ///   [ConnectionState.cs]
      ///   public class ConnectionState : Payload
      ///   {
      ///      public string Address { get; set; }
      ///      public int Port { get; set; }
      ///
      ///      public ConnectionState(string address, int port)
      ///      {
      ///         Address = address;
      ///         Port = port;
      ///      }
      ///   }
      ///  
      ///   [Publisher.cs]
      ///   public class Publisher
      ///   {
      ///      public void Run()
      ///      {
      ///         // Publish the Connection event data      
      ///         EventHub.Publish&lt;ConnectionState&gt;(new ConnectionState("127.0.0.1","8080"));
      ///      }
      ///   }
      ///
      ///   [Subscriber.cs]
      ///   public class Subscriber
      ///   {
      ///      public void Run()
      ///      {
      ///         // Subscribes to the Connection Event 
      ///         EventHub.Subscribe&lt;ConnectionState&gt;(SubscribedMethod);
      ///      }
      ///      
      ///      public void SubscribedMethod(ConnectionState state)
      ///      {
      ///         Console.WriteLine(string.Format("http://{0}:{1}/", state.Address, state.Port));
      ///      }
      ///   }
      /// </code>
      /// </example>
      public static void Subscribe<TPayloadData>(Action<TPayloadData> callback, bool runOnUiThread = true)
          where TPayloadData : Payload
      {

         callback.ThrowArgumentNullExceptionIfNull("callback");

         lock (eventDictionary)
         {
            string eventName = typeof(TPayloadData).Name.ToLower();

            EventBase<TPayloadData> publishedType;

            if (eventDictionary.ContainsKey(eventName))
            {
               publishedType = AddSubscriberToEvent(callback, eventName);
            }
            else
            {
               publishedType = CreateEventAndAddSubscriber(callback, eventName);
            }

            if (publishedType != null && publishedType.LastPublishedData.Any())
            {
               PerformPublish<TPayloadData>(publishedType, publishedType.LastPublishedData, runOnUiThread);
               publishedType.LastPublishedData.RemoveAll(p => p.Fulfilled == false && p.Disposable);
            }
         }
      }

      /// <summary>
      /// Creates the event and adds payload.
      /// </summary>
      /// <typeparam name="TPayloadData">The type of the t payload data.</typeparam>
      /// <param name="payload">The payload.</param>
      /// <param name="eventName">Name of the event.</param>
      /// <param name="isDisposable">Indicates if the published data can be removed when its subscription has been fulfilled.</param>
      private static void CreateEventAndAddPayload<TPayloadData>(TPayloadData payload, string eventName, bool isDisposable = true)
         where TPayloadData : Payload
      {
         EventBase<TPayloadData> publishedType = new EventBase<TPayloadData>
         {
            EventName = eventName,
         };

         Log("Publish:Event '{0}' not found, creating with id '{1}'", eventName, publishedType.UniqueEventId);
         publishedType.LastPublishedData = new List<PublishedData>();
         if (isDisposable == false)
         {
            // clear all other publish data. If we are saying that we want the data to persist that we only 
            // want a single entry for the data, we don't it to become an huge stack of values
            publishedType.LastPublishedData.Clear();
            publishedType.LastPublishedData.Add(new PublishedData { Data = payload, Disposable = false });
         }
         else
         {
            publishedType.LastPublishedData.Add(new PublishedData { Data = payload, Disposable = true });
         }

         lock (eventDictionary)
         {
            eventDictionary.Add(eventName, publishedType);
         }
      }

      /// <summary>
      /// Gets the event and publish.
      /// </summary>
      /// <typeparam name="TPayloadData">The type of the t payload data.</typeparam>
      /// <param name="payload">The payload.</param>
      /// <param name="runOnUiThread">if set to <c>true</c> [run on UI thread].</param>
      /// <param name="eventName">Name of the event.</param>
      /// <param name="isDisposable">Indicates if the published data can be removed when its subscription has been fulfilled.</param>
      private static void GetEventAndPublish<TPayloadData>(TPayloadData payload, bool runOnUiThread, string eventName, bool isDisposable = true)
         where TPayloadData : Payload
      {
         EventBase<TPayloadData> publishedType = eventDictionary[eventName] as EventBase<TPayloadData>;

         if (publishedType != null)
         {
            Log("Publish:Event '{0}' found with id '{1}'", eventName, publishedType.UniqueEventId);

            publishedType.LastPublishedData.Add(new PublishedData() { Data = payload, Disposable = isDisposable });
            PerformPublish(publishedType, publishedType.LastPublishedData, runOnUiThread);

            publishedType.LastPublishedData.RemoveAll(p => p.Fulfilled == true && p.Disposable == true);
         }
      }

      /// <summary>
      /// Creates the event and add subscriber.
      /// </summary>
      /// <typeparam name="TPayloadData">The type of the t payload data.</typeparam>
      /// <param name="callback">The callback.</param>
      /// <param name="eventName">Name of the event.</param>
      /// <returns>A publication EventBase object which is the basis of all events</returns>
      private static EventBase<TPayloadData> CreateEventAndAddSubscriber<TPayloadData>(
         Action<TPayloadData> callback, string eventName) where TPayloadData : Payload
      {
         EventBase<TPayloadData> publishedType;
         publishedType = new EventBase<TPayloadData>
         {
            EventName = eventName
         };

         publishedType.Subscribers.Add(callback);
         eventDictionary.Add(eventName, publishedType);

         Log("Created Publisher for event:'{0}', Waiting for subscribers with id '{1}'", eventName,
            publishedType.UniqueEventId);
         return publishedType;
      }

      /// <summary>
      /// Adds the subscriber to event.
      /// </summary>
      /// <typeparam name="TPayloadData">The type of the t payload data.</typeparam>
      /// <param name="callback">The callback.</param>
      /// <param name="eventName">Name of the event.</param>
      /// <returns>Adds a subscriber to the selected event and return the EventBase object</returns>
      private static EventBase<TPayloadData> AddSubscriberToEvent<TPayloadData>(
         Action<TPayloadData> callback, string eventName) where TPayloadData : Payload
      {
         EventBase<TPayloadData> publishedType;
         publishedType = eventDictionary[eventName] as EventBase<TPayloadData>;
         if (publishedType != null)
         {
            publishedType.Subscribers.Add(callback);
            Log("Subscriber added for event:'{0}' with id '{1}'", eventName, publishedType.UniqueEventId);
         }
         return publishedType;
      }

      /// <summary>
      /// Performs the publish.
      /// </summary>
      /// <typeparam name="TPayloadData">The type of the t payload data.</typeparam>
      /// <param name="publisher">The publisher.</param>
      /// <param name="payload">The payload.</param>
      /// <param name="runOnUiThread">if set to (default)<c>true</c> the publication executes on the UI Thread..</param>
      /// <exception cref="ArgumentNullException">if the publisher is null</exception>
      private static void PerformPublish<TPayloadData>(EventBase<TPayloadData> publisher,
         List<PublishedData> payload, bool runOnUiThread = true) where TPayloadData : Payload
      {
         publisher.ThrowArgumentNullExceptionIfNull("publisher");

         if (payload == null)
         {
            return;
         }

         Log("Publish for event:'{0}' with event id: [{1}] with payload [{2}]", publisher.EventName, publisher.UniqueEventId, payload);

         publisher.Execute(runOnUiThread);
      }

      /// <summary>
      /// Logs the specified message.
      /// </summary>
      /// <param name="message">The message.</param>
      /// <param name="args">The arguments.</param>
      private static void Log(string message, params object[] args)
      {
         Trace.WriteLineIf(TraceEnabled, string.Format(message, args));
      }

      /// <summary>
      /// Runs the on UI thread.
      /// </summary>
      /// <param name="process">The process.</param>
      /// <param name="runOnUiThread">if set to <c>true</c> [run on UI thread].</param>
      private static void ExecuteDelegate(Action process, bool runOnUiThread = true)
      {
         if (runOnUiThread)
         {
            Log("Executing On the Ui Thread with thread id: {0}",
               Application.Current.Dispatcher.Thread.ManagedThreadId);
            Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, process);
         }
         else
         {
            Log("Executing On the None Ui Thread with thread id: {0}", Thread.CurrentThread.ManagedThreadId);
            process();
         }
      }
   }
}
